﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using StoCombatLogHUD.Toolkit;
using StoCombatLogHUD.Toolkit.ExtensionMethods;
using Timer = System.Threading.Timer;

namespace StoCombatLogHUD.Parsing
{
	/// <summary>
	/// The main parsing logic.
	/// </summary>
	public class CombatLogParser : IDisposable
	{
		/// <summary>
		/// Gets or sets the log file path.
		/// </summary>
		/// <value>
		/// The log file path.
		/// </value>
		public string LogFilePath { get; set; }

		/// <summary>
		/// The combat log entries.
		/// </summary>
		private readonly Queue<CombatLogEntry> combatLogEntries = new Queue<CombatLogEntry>();

		/// <summary>
		/// EventArgs for <see cref="CombatLogEntriesChanged"/>.
		/// </summary>
		public class CombatLogEntriesChangedEventArgs : EventArgs
		{
			public IEnumerable<CombatLogEntry> NewEntries { get; set; }
		}

		/// <summary>
		/// Occurs when the combat log entries have changed.
		/// </summary>
		public event EventHandler<CombatLogEntriesChangedEventArgs> CombatLogEntriesChanged;

		/// <summary>
		/// Raised whenever an exception occured.
		/// </summary>
		public event EventHandler<ThreadExceptionEventArgs> ExceptionOccured;

		/// <summary>
		/// Gets or sets the parsing task.
		/// </summary>
		/// <value>
		/// The parsing task.
		/// </value>
		private Task ParsingTask { get; set; }

		/// <summary>
		/// Gets or sets the parsing cancellation token.
		/// </summary>
		/// <value>
		/// The parsing cancellation token.
		/// </value>
		private CancellationTokenSource ParsingTaskCancellationToken { get; set; }

		/// <summary>
		/// Initializes a new instance of the <see cref="CombatLogParser"/> class.
		/// </summary>
		/// <param name="logFilePath">The log file path.</param>
		public CombatLogParser(string logFilePath)
		{
			LogFilePath = logFilePath;

			// Create the combatlog if it's in the STO directory and non-existing yet
			if (GameHelpers.IsValidStarTrekOnlineLogDirectory(LogFilePath))
			{
				if (!File.Exists(LogFilePath))
					using (File.Create(LogFilePath))
					{
					}
			}

			if (!File.Exists(LogFilePath))
				throw new FileNotFoundException();
		}

		/// <summary>
		/// Begins the parsing operation
		/// </summary>
		public void BeginParse()
		{
			Task.Factory
			    .StartNew(EndParse)
			    .ContinueWith(task => WaitForPendingOperationsToComplete())
			    .ContinueWith(task => PrepareParsingOperation())
			    .ContinueWith(task =>
				                  {
					                  ParsingTask =
						                  Task.Factory
						                      .StartNew(() => ParsingLoop(task.Result))
						                      .ContinueWith(
							                      parsingTask =>
							                      ExceptionOccured.InvokeNullSafe(this, new ThreadExceptionEventArgs(parsingTask.Exception)),
							                      TaskContinuationOptions.OnlyOnFaulted);
				                  })
			    .ContinueWith(
				    task =>
				    ExceptionOccured.InvokeNullSafe(this, new ThreadExceptionEventArgs(task.Exception)),
				    TaskContinuationOptions.OnlyOnFaulted);
		}

		/// <summary>
		/// Ends a running parsing operation
		/// </summary>
		public void EndParse()
		{
			if (ParsingTask.IsRunning() && !ParsingTaskCancellationToken.IsCancellationRequested)
				ParsingTaskCancellationToken.Cancel();
		}

		/// <summary>
		/// Exports the current combat log to <param name="destinationFile"></param>.
		/// </summary>
		/// <param name="destinationFile">The destination file.</param>
		/// <param name="startTimeStamp">The start time stamp at which to begin the copy operation.</param>
		public void ExportCombatLog(string destinationFile, DateTime startTimeStamp)
		{
			var outputStream = new FileStream(destinationFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read);
			outputStream.SetLength(0);
			var inputStream = new FileStream(LogFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
			
			using (var outputStreamWriter = new StreamWriter(outputStream))
			{
				using (var inputStreamReader = new StreamReader(inputStream))
				{
					var lineCache = string.Empty;
					while (!inputStreamReader.EndOfStream)
					{
						var nextLine = inputStreamReader.ReadLine();
						nextLine = PerformWorkArounds(nextLine, ref lineCache);
						var nextEntry = ParseEntry(nextLine);
						if (nextEntry == null)
							continue;

						if (nextEntry.Timestamp < startTimeStamp)
							continue;

						outputStreamWriter.Write(nextEntry + "\n");
					}
				}
			}
		}

		/// <summary>
		/// The main parsing loop.
		/// </summary>
		/// <param name="logFileStream">The log file stream.</param>
		private void ParsingLoop(Stream logFileStream)
		{
			using (new Timer(NotificationTimerRoutine, null, new TimeSpan(), notificationTimerFrequency))
			{
				using (var streamReader = new StreamReader(logFileStream))
				{
					var lineCache = string.Empty;
					while (!ParsingTaskCancellationToken.IsCancellationRequested)
					{
						var nextLine = streamReader.ReadLine();
						nextLine = PerformWorkArounds(nextLine, ref lineCache);
						if (string.IsNullOrEmpty(nextLine))
						{
							Thread.Sleep(50);
							continue;
						}

						var nextEntry = ParseEntry(nextLine);
						if (nextEntry == null)
							continue;

						lock (combatLogEntries)
						{
							combatLogEntries.Enqueue(nextEntry);
						}
						combatLogEntriesChangedFlag = true;
					}
				}
			}
		}

		/// <summary>
		/// Performs some workarounds for line parsing.
		/// </summary>
		/// <param name="nextLine">The next line.</param>
		/// <param name="lineCache">The line cache.</param>
		/// <returns></returns>
		private static string PerformWorkArounds(string nextLine, ref string lineCache)
		{
			if (string.IsNullOrEmpty(nextLine))
				return null;

			// STO seems to cut lines apart sometimes.
			// If it does, the cut seems to start after a space.
			// TODO: Not clear whether this is the case all the time, needs to be verified
			if (nextLine.EndsWith(" "))
			{
				lineCache += nextLine;
				return null;
			}

			nextLine = lineCache + nextLine;
			lineCache = null;
			return nextLine;
		}

		/// <summary>
		/// Gets the new combat log entries.
		/// </summary>
		/// <returns></returns>
		private IEnumerable<CombatLogEntry> GetNewCombatLogEntries()
		{
			lock (combatLogEntries)
			{
				while (combatLogEntries.Count > 0)
					yield return combatLogEntries.Dequeue();
			}
		}

		#region Notification Timer

		/// <summary>
		/// The combat log entries changed flag.
		/// </summary>
		private bool combatLogEntriesChangedFlag;

		/// <summary>
		/// The notification timer frequency.
		/// </summary>
		private readonly TimeSpan notificationTimerFrequency = new TimeSpan(0, 0, 0, 0, 50);

		/// <summary>
		/// The notifications timer routine.
		/// Invokes the <see cref="CombatLogEntriesChanged"/> event when log entries have changed
		/// </summary>
		/// <param name="state">The state.</param>
		private void NotificationTimerRoutine(object state)
		{
			if (!combatLogEntriesChangedFlag)
				return;

			combatLogEntriesChangedFlag = false;
			CombatLogEntriesChanged.InvokeNullSafe(this,
			                                       new CombatLogEntriesChangedEventArgs
				                                       {
					                                       NewEntries = GetNewCombatLogEntries().ToArray()
				                                       });
		}

		#endregion

		/// <summary>
		/// Parses one single entry.
		/// </summary>
		/// <param name="line">The line.</param>
		/// <returns></returns>
		/// <exception cref="FormatException">
		/// </exception>
		private static CombatLogEntry ParseEntry(string line)
		{
			try
			{
				return CombatLogEntry.FromString(line);
			}
			catch
			{
				File.AppendAllText("Error.log", string.Format("Parsing error ecountered while trying to parse: \"{0}\"{1}", line, Environment.NewLine));
			}

			return null;
		}

		/// <summary>
		/// Waits for pending operations to complete.
		/// </summary>
		private void WaitForPendingOperationsToComplete()
		{
			if (!ParsingTask.IsRunning())
				return;

			try
			{
				ParsingTask.Wait();
			}
// ReSharper disable EmptyGeneralCatchClause
			catch (Exception)
// ReSharper restore EmptyGeneralCatchClause
			{
			}
		}

		/// <summary>
		/// Prepares the parsing operation.
		/// </summary>
		/// <returns></returns>
		private FileStream PrepareParsingOperation()
		{
			lock (combatLogEntries)
			{
				combatLogEntries.Clear();
			}
			ParsingTaskCancellationToken = new CancellationTokenSource();

			return new FileStream(LogFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
		}

		#region IDisposable

		/// <summary>
		/// Gets a value indicating whether this instance is disposed.
		/// </summary>
		/// <value>
		/// <c>true</c> if this instance is disposed; otherwise, <c>false</c>.
		/// </value>
		public bool IsDisposed { get; private set; }

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			if (IsDisposed)
				return;

			EndParse();
			WaitForPendingOperationsToComplete();

			IsDisposed = true;
		}

		#endregion
	}
}